/*
 *  Copyright (c) 2013, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

package com.facebook.rebound;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * BaseSpringSystem maintains the set of springs within an Application context. It is responsible for
 * Running the spring integration loop and maintains a registry of all the Springs it solves for.
 * In addition to listening to physics events on the individual Springs in the system, listeners
 * can be added to the BaseSpringSystem itself to provide pre and post integration setup.
 */
public class BaseSpringSystem {

    private final Map<String, Spring> mSpringRegistry = new HashMap<String, Spring>();
    private final Set<Spring> mActiveSprings = new CopyOnWriteArraySet<Spring>();
    private final SpringLooper mSpringLooper;
    private final CopyOnWriteArraySet<SpringSystemListener> mListeners = new CopyOnWriteArraySet<SpringSystemListener>();
    private boolean mIdle = true;

    /**
     * create a new BaseSpringSystem
     * @param springLooper parameterized springLooper to allow testability of the
     *        physics loop
     */
    public BaseSpringSystem(SpringLooper springLooper) {
        if (springLooper == null) {
            throw new IllegalArgumentException("springLooper is required");
        }
        mSpringLooper = springLooper;
        mSpringLooper.setSpringSystem(this);
    }

    /**
     * check if the system is idle
     * @return is the system idle
     */
    public boolean getIsIdle() {
        return mIdle;
    }

    /**
     * create a spring with a random uuid for its name.
     * @return the spring
     */
    public Spring createSpring() {
        Spring spring = new Spring(this);
        registerSpring(spring);
        return spring;
    }

    /**
     * get a spring by name
     * @param id id of the spring to retrieve
     * @return Spring with the specified key
     */
    public Spring getSpringById(String id) {
        if (id == null) {
            throw new IllegalArgumentException("id is required");
        }
        return mSpringRegistry.get(id);
    }

    /**
     * return all the springs in the simulator
     * @return all the springs
     */
    public List<Spring> getAllSprings() {
        Collection<Spring> collection = mSpringRegistry.values();
        List<Spring> list;
        if (collection instanceof List) {
            list = (List<Spring>) collection;
        } else {
            list = new ArrayList<Spring>(collection);
        }
        return Collections.unmodifiableList(list);
    }

    /**
     * Registers a Spring to this BaseSpringSystem so it can be iterated if active.
     * @param spring the Spring to register
     */
    void registerSpring(Spring spring) {
        if (spring == null) {
            throw new IllegalArgumentException("spring is required");
        }
        if (mSpringRegistry.containsKey(spring.getId())) {
            throw new IllegalArgumentException("spring is already registered");
        }
        mSpringRegistry.put(spring.getId(), spring);
    }

    /**
     * Deregisters a Spring from this BaseSpringSystem, so it won't be iterated anymore. The Spring should
     * not be used anymore after doing this.
     *
     * @param spring the Spring to deregister
     */
    void deregisterSpring(Spring spring) {
        if (spring == null) {
            throw new IllegalArgumentException("spring is required");
        }
        mActiveSprings.remove(spring);
        mSpringRegistry.remove(spring.getId());
    }

    /**
     * update the springs in the system
     * @param deltaTime delta since last update in millis
     */
    void advance(double deltaTime) {
        for (Spring spring : mActiveSprings) {
            // advance time in seconds
            if (spring.systemShouldAdvance()) {
                spring.advance(deltaTime / 1000.0);
            } else {
                mActiveSprings.remove(spring);
            }
        }
    }

    /**
     * loop the system until idle
     */
    public void loop(double ellapsedMillis) {
        for (SpringSystemListener listener : mListeners) {
            listener.onBeforeIntegrate(this);
        }
        advance(ellapsedMillis);
        if (mActiveSprings.isEmpty()) {
            mIdle = true;
        }
        for (SpringSystemListener listener : mListeners) {
            listener.onAfterIntegrate(this);
        }
        if (mIdle) {
            mSpringLooper.stop();
        }
    }

    /**
     * This is used internally by the {@link Spring}s created by this {@link BaseSpringSystem} to notify
     * it has reached a state where it needs to be iterated. This will add the spring to the list of
     * active springs on this system and start the iteration if the system was idle before this call.
     * @param springId the id of the Spring to be activated
     */
    void activateSpring(String springId) {
        Spring spring = mSpringRegistry.get(springId);
        if (spring == null) {
            throw new IllegalArgumentException("springId " + springId
                                               + " does not reference a registered spring");
        }
        mActiveSprings.add(spring);
        if (getIsIdle()) {
            mIdle = false;
            mSpringLooper.start();
        }
    }

    /** listeners **/

    public void addListener(SpringSystemListener newListener) {
        if (newListener == null) {
            throw new IllegalArgumentException("newListener is required");
        }
        mListeners.add(newListener);
    }

    public void removeListener(SpringSystemListener listenerToRemove) {
        if (listenerToRemove == null) {
            throw new IllegalArgumentException("listenerToRemove is required");
        }
        mListeners.remove(listenerToRemove);
    }

    public void removeAllListeners() {
        mListeners.clear();
    }
}
